[pull] canary from vercel:canary - #1333
Merged
Merged
Conversation
We are about to reduce the number of these entries, and each of those changes should arrive with a diff that shows which ones it removes or merges. That is what these fixtures record. The existing behavioral suites stay responsible for regressions, and they do detect a routing change of this kind, e.g. a build that dropped the `.rsc` entries of fallback shells would fail a root param case in `segment-cache/prefetch-app-shell`. These snapshots add the part those suites may not distinguish, since an entry that collapses into a less specific match can still serve a response that renders the same content. One fixture covers Cache Components with root params, the shape that grows with the number of root param combinations. The other covers what does not depend on Cache Components: the `.rsc` and plain entry pair that every dynamic app page receives, a route handler, pages that share one shape and differ in a static last segment, and a `fallback: false` pages router route whose plain entry carries a preview bypass condition that its `.rsc` sibling does not. It also holds a proxy next to static pages router pages, which adds one entry per page and is the only case here whose count grows with the number of pages rather than with the number of route shapes. A third test builds the first fixture again under a base path and asserts the prefix on every entry. The projection stays narrow so that unrelated build output leaves it alone. Both fixtures pin `cacheComponents` and `generateBuildId`, because CI would otherwise vary them.
For a dynamic app page that has a fallback shell, say `/[lang]/[slug]` with a shell for the root param value `de`, the build emits three routes to an adapter. Simplified, with the request pattern on the left and the artifact it resolves to on the right: ```diff - /de/<slug>.segments/$d$lang/$d$slug<seg> -> /de/[slug].segments/$d$lang/$d$slug<seg> /de/<slug><.rsc|.segments/*.segment.rsc> -> /de/[slug]<matched suffix> /de/<slug> -> /de/[slug] ``` This change removes the first one. The second already covers it: its suffix group accepts `.segments/<path>.segment.rsc` as well as `.rsc`, and it copies the matched suffix into the destination, so a per-segment request resolves to the same artifact either way. That second route is also the only one that ever answered `_tree` and `_full` requests, because a per-segment route pins one literal segment path in its regex. A `_tree` prefetch for `/de/<slug>` resolves to `/de/[slug].segments/_tree.segment.rsc`, which the per-segment route cannot produce. So this removes a duplicate, not a mechanism. Only fallback shells emit per-segment routes, because their artifacts sit under an unresolved param and need a rewrite to reach. Segment artifacts for a concrete prerendered path need none, since a request for that path matches them directly. Apps with many shells therefore lose close to a third of their routes. We measured an in-progress feature branch of the v0 chat app, which enumerates precomputed flag, locale and device permutations in `generateStaticParams` for a top-level dynamic segment and so multiplies every route below it. Building that branch before and after the change removes 32% of its routes, adds none, and changes nothing else once the build ID is normalized. This also adds `experimental.collapseAdapterRoutes`. It defaults to `true`, and it controls this collapse together with the ones that follow in this stack. A build that sets it to `false` emits the same route table as a build without any change in this stack. `prefetchSegmentDataRoutes` stays in `routes-manifest.json`. A build that does not use the adapter reads the field from that manifest and derives the same routes from it. This change therefore leaves that path alone. **Verified upstack with a [full deploy test run](https://github.com/vercel/next.js/actions/runs/32540912007).**
…97726) A dynamic app page in an app with an app directory needs two routes today. One matches a request for the page, and one matches a request for its `.rsc` payload or a per-segment prefetch. Simplified, with the request pattern on the left and the artifact it resolves to on the right: ```diff - /de/<slug><.rsc|.segments/*.segment.rsc> -> /de/[slug]<matched suffix> - /de/<slug> -> /de/[slug] + /de/<slug><.rsc|.segments/*.segment.rsc|> -> /de/[slug]<matched suffix> ``` One entry now serves both, because the suffix group gains an empty final alternative. The group therefore always matches. It captures an empty string for a request that carries no suffix, and the destination copies whatever it captured, so a plain request resolves to the page and a suffixed request resolves to the artifact it asks for. The parameter is matched lazily, so a request for `/de/other.rsc` still prefers the shorter parameter and the `.rsc` suffix over a parameter that swallows the suffix. The empty alternative is deliberate, and an optional group would not be equivalent. A consumer of these entries can resolve the placeholders in a destination from the match result rather than from the pattern. A group that does not take part in the match is then absent from that result, and the literal text `$rscSuffix` survives into the destination. A group that always takes part avoids depending on how any one consumer treats an absent key, and the cost of getting it wrong is a 404 on every plain navigation to every dynamic route. One case keeps two entries. A pages router route with `fallback: false` requires the preview cookies on the request for the page, and not on the request for its `.rsc` payload. An entry carries one set of conditions and cannot express that difference, so the merge applies only when both forms agree on their conditions. Every app that has an app directory therefore halves the entries for its dynamic routes, whether or not it uses Cache Components. We measured the same in-progress feature branch of the v0 chat app as the previous change in this stack, which enumerates precomputed flag, locale and device permutations in `generateStaticParams` for a top-level dynamic segment and so multiplies every route below it. Every dynamic entry on that branch merges. Building it before and after the change removes 48% of the routes that the previous change left, replaces each merged pair with a single entry, and changes nothing else once the build ID is normalized. Across both changes that branch loses 65% of its routes. This collapse follows `experimental.collapseAdapterRoutes`, which the previous change in this stack added. A build that sets it to `false` emits a separate entry for each form. **Verified with a [full deploy test run](https://github.com/vercel/next.js/actions/runs/32540912007).**
…7728) The existing fixtures take a single root param, so they do not cover the shapes that the fallback shell entries can take. This adds fixtures and suites for three more shapes, and pins the entries that each one produces. The first has a root layout that takes two root params, and returns three combinations of them rather than the full product of four. An entry that held each root param position separately would also match `sparse/de`, a combination the build never prerendered, and a request for it would resolve to an output that does not exist. One combination is `acme.one-two,three`, which carries the characters a regex treats as special, and the snapshot shows the build escaping them. The second builds that same app with a single combination, so each source page has one fallback shell. The third has no root params at all. Its root layout sits above the dynamic segments, so `team` and `locale` are ordinary dynamic params that `generateStaticParams` enumerates on a nested layout. The build then produces two shapes of entry for one source page: entries that resolve both params, such as `/sparse/en/posts/[id]`, and entries that resolve only the first, such as `/sparse/[locale]/posts/[id]`. The order of those entries carries the behavior, because a request for `/sparse/en/posts/1` has to reach the output that resolves both params rather than the one that resolves only the first. The snapshot pins that order. Two comments on the Cache Components suite were wrong, and this corrects them. That fixture takes one root param with two values, and not two root params. Its entries also no longer come in pairs, because one entry serves the request for the page, the request for its `.rsc` payload, and a per-segment prefetch.
A fallback shell repeats the whole path of its source page and resolves the leading params to concrete values. Take `/[team]/[locale]/[slug]`, where the build prerenders three combinations of the two leading params. It emits one entry per combination, and the entries differ only in that leading part of the path: ```diff - /acme/en/<slug><suffix> -> /acme/en/[slug]<matched suffix> - /acme/de/<slug><suffix> -> /acme/de/[slug]<matched suffix> - /globex/en/<slug><suffix> -> /globex/en/[slug]<matched suffix> + /(?<shellPrefix>acme/en|acme/de|globex/en)/<slug><suffix> -> /$shellPrefix/[slug]<matched suffix> ``` One entry now serves them all. Its pattern lists the leading part of each shell path as an alternative, and its destination copies whichever one matched. Those alternatives are complete, and that matters. A pattern that offered a choice per param instead, `(acme|globex)/(en|de)`, would also match `globex/de`. The build never prerendered that combination, so a request for it would resolve to an output that does not exist, and it would then fall through to whichever route claims the rewritten path. An entry serves neighbours in the manifest, and only those. It takes the position of the first shell that it replaces, so every replaced shell keeps its place relative to the routes around it. Any other route between two shells ends the run, because an entry that reached across it would move ahead of a route that a request matches first. The shells of a run also have to agree on `fallback: false`, because an entry carries one set of conditions. A source page can therefore hold several runs, and a shell can belong to none. That happens when the build resolves a different number of params for neighbouring shells, which leaves them with different paths after the resolved part. This removes the multiplier that the number of prerendered combinations applies to every route below the resolved params. We measured the same in-progress feature branch of the v0 chat app as the previous changes in this stack. Building it before and after this change removes 87% of the routes that those changes left. Across the stack that branch loses 95% of its routes. This collapse follows `experimental.collapseAdapterRoutes`, which an earlier change in this stack added. A build that sets it to `false` emits one entry per shell. **Verified with a [full deploy test run](https://github.com/vercel/next.js/actions/runs/32577936779).**
`experimental.collapseAdapterRoutes` now defaults to `false`, so a build keeps one entry per route unless the project opts in. The changes below it in this stack stay as they are, and a project that sets the option to `true` gets the route table that they produce. The default starts off so that we can dogfood the behavior on selected apps before every build gets it. The default moves back to `true` once enough apps have run with it. The four fixtures under `test/production/app-dir/adapter-dynamic-routes` set the option, because their snapshots pin the collapsed table. A build of the Cache Components fixture without the option emits the 27 entries that the first commit in this stack recorded, which is what the option now turns off.
**View diff without whitespaces** Fix remaining cases of #97666 - Mostly replacing async blocks with async closures - And second commit: some more making closures sync where possible
…96536) While writing an adapter against `16.3.0-canary.107` I noticed the adapter docs are missing two fields that the shipped `NextAdapter` types already have. **`assetsHashes`** — every `PAGES` / `PAGES_API` / `APP_PAGE` / `APP_ROUTE` / `MIDDLEWARE` output carries it right next to `assets`, and it's declared with a doc comment in `build-complete.d.ts`, but none of the five shapes in Output Types mention it. **`routing.middlewareMatchers`** — dumping the `routing` object from a real `onBuildComplete` call gives: ``` afterFiles, beforeFiles, beforeMiddleware, dynamicRoutes, fallback, middlewareMatchers, onMatch, rsc, shouldNormalizeNextData ``` but the docs list eight of those nine, everywhere the interface appears: the Creating an Adapter snippet (which says "The interface is defined as follows"), the API Reference parameter list, and Routing Information. This one feels worth fixing soon — an adapter that does its own request matching from the documented fields alone has no way to decide when middleware should run. My guess for why nobody has hit it: if you pass `routes: routing` wholesale into `resolveRoutes` from `@next/routing`, everything works without ever looking at the field. The wording I added comes from the doc comments in the shipped types, not my own descriptions. `middlewareMatchers` is inserted where the type puts it (right after `beforeMiddleware`). For what it's worth: I checked the rest of the section against the same build while I was at it — the `output: 'export'` behavior, the prerender classification fields, `pprChain.headers`, the fallback fields, the immutable-assets flow, and the `@next/routing` params/result — and everything else matched the docs. These two were the only gaps I found.
## Summary The basePath external-navigation test depended on loading Google and recognizing its DOM before exercising browser history. That made the test sensitive to public network availability and third-party page changes; the reported failure was confirmed in the Safari/WebKit CI run on the initial attempt and both retries. For local and CI test-server runs, this replaces the public dependency with a minimal page served from a separate local origin, preserving the cross-origin, full-document navigation being tested. Deploy mode remains enabled and uses a deploy-accessible external target because deployed browsers cannot reach the test runner. Both paths now synchronize on browser origin and exact restored-page state instead of third-party DOM or a fixed sleep. ## Verification - Focused production/start-mode test passed three consecutive times in Firefox - Focused production/start-mode test passed in Chromium - Prettier and ESLint passed for the changed files - `pnpm build-all` - Not run: Safari/WebKit locally (Playwright’s unsupported Amazon Linux fallback requires incompatible Ubuntu system libraries; the Safari CI job is the definitive validation) - Not run: deploy-mode execution locally (deployment credentials unavailable; the deploy path remains enabled for CI) <!-- NEXT_JS_LLM --> Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
### What? Separate trace-graph module path collection from module content hashing. Exclusion filtering now requests only cached module identifiers, while full NFT data still layers content hashes on top when they are required. ### Why? Applying output-file-tracing exclusion globs needs module paths but not content hashes. Keeping these computations separate avoids requesting full-graph hashes solely to decide which modules should be skipped. This is an inspection experiment, not a demonstrated performance improvement. A seven-sample release A/B left the median compilation phase unchanged at 2700 ms. ### How? A private identifier-only graph task owns the existing DFS traversal. `traced_module_data_for_graph` reuses those identifiers and computes hashes for its full result. `traced_modules_for_entries` drops the unused hash-salt input and consumes only identifiers for glob matching. ### Verification - `cargo fmt -- --check` - `cargo check -p next-api` - `next-server.js.nft.json`: byte-identical SHA-256 `da4c5a494efbbb38eb966066115de96adb5d0ec693fba17bbed669c99cafae43` - `next-minimal-server.js.nft.json`: byte-identical SHA-256 `7c58d40dc2ecbe6ddd26dd0c602467d80f1a8ea3320e2c6e8e5f4d6be14156cd` - Not run: clippy, Rust test suite, or integration tests (inspection experiment) <!-- NEXT_JS_LLM --> Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
Some cleanup, this was never used: - `config` from `getStaticInfoIncludingLayouts` - `getServerSideProps` and `getStaticProps` from pages router
### What?
Exposes the `cacheHandlers` types (`CacheHandler`, `CacheEntry`) as
types-only exports from `next/cache`, so handlers can be checked against
the real interface:
```ts
import type { CacheHandler } from 'next/cache'
```
### Why?
Custom cache handler authors currently import `CacheHandler` and
`CacheEntry` from `next/dist/server/lib/cache-handlers/types` (an
internal path that can move between versions) or hand-copy the
interfaces, which drift silently across releases.
### Motivation
I'm working on a custom community cache handler and it'd be great to
import the types directly and keep testing against the source of truth,
rather than maintaining a hand-copied mirror that has to be re-checked
on every Next.js release.
Raised in discussion #96356.
### How?
- Adds a type-only `export type { CacheHandler, CacheEntry }` to
`packages/next/cache.d.ts`, re-exported from
`./dist/server/lib/cache-handlers/types`. No new subpath or
`package.json` `"files"` entries needed since `next/cache` already
ships.
- Docs: `cacheHandlers.mdx` now shows the public `next/cache` import
instead of the GitHub source links.
- Tests: a `satisfies CacheHandler` / `satisfies CacheEntry` fixture in
the `typescript-basic` typechecking suite (runs `tsc` against the
installed package), and the `use-cache-custom-handler` e2e fixture's
JSDoc now uses the public import.
(Originally proposed as a separate `next/cache-handlers` types-only
subpath, following the `next/types` pattern moved the export into
`next/cache` per review.)
Related: #96356
closes #97781 (only created to run deploy tests)
### What? Reduce the binary-size cost of TurboMalloc's allocation instrumentation while preserving its hot-path performance and accounting semantics. This adds a focused Criterion benchmark for TurboMalloc's raw allocation paths and moves only the uncommon global counter refill/spill operations behind cold, non-inlined helpers. Thread-local allocation counters, buffer checks, and common buffer adjustments remain inline at allocation sites. ### Why? Rust gives global allocator methods special inlining treatment. TurboMalloc's thread-local accounting was consequently duplicated across allocation and deallocation call sites in `next-swc`, contributing several megabytes of machine code. Moving the whole accounting path out of line recovered most of that space but made small allocations slower. Splitting only the rare atomic paths provides a smaller size reduction without that hot-path cost. ### How? The common path remains explicitly inlineable through `add`, `remove`, `update`, and the thread-local access helper. When the local buffer must be refilled or spilled, it calls a `#[cold] #[inline(never)]` helper that performs the global relaxed atomic update. The arithmetic, thresholds, target buffer values, counter increments, and atomic ordering are unchanged. The counter test now also covers realloc growth/spill and shrink/flush behavior. ### Benchmark results Linux x86_64 Intel Xeon, Rust 1.99.0-nightly / LLVM 22.1.8. Native artifacts used the repository's production release command. Direct application measurements used clean `bench/heavy-npm-deps` `next build --turbopack` runs, with 8 samples per arm and rotated/interleaved ordering. #### Binary size | alternative | raw artifact delta | `.text` delta | |---|---:|---:| | No instrumentation | −6,260,168 B (−4.23%) | −5,466,304 B (−7.69%) | | `inline(never)` on `add` / `remove` / `update` | −4,338,856 B (−2.93%) | −4,342,464 B (−6.11%) | | `inline(never)` on whole `GlobalAlloc` methods | −4,153,792 B (−2.80%) | −3,714,688 B (−5.23%) | | **Cold atomic refill/spill helpers (this PR)** | **−502,864 B (−0.34%)** | **−732,736 B (−1.03%)** | The current artifact was approximately 148.1 MB raw with 71.1 MB of `.text`. The retained split recovers 8% of the raw and 13% of the `.text` saving from removing instrumentation entirely. #### Direct `next build --turbopack` | alternative | mean effect vs current | bootstrap 95% CI | permutation p | |---|---:|---:|---:| | No instrumentation | −0.70% | [−2.32%, +1.12%] | 0.466 | | `inline(never)` on `add` / `remove` / `update` | **+3.53%** | **[+0.65%, +6.43%]** | 0.0448 | | `inline(never)` on whole `GlobalAlloc` methods | +1.02% | [−2.03%, +4.44%] | 0.586 | | **Cold atomic refill/spill helpers (this PR)** | **−0.18%** | **[−3.44%, +3.61%]** | **0.929** | No application-level difference was detected for the retained split or the whole-method placement. The helper-level noinline placement showed evidence of a modest regression. #### Focused allocator Criterion benchmark Percent changes are lower-is-better. Where two figures are shown, they are independent runs in reversed order. | alternative | alloc + dealloc | alloc + realloc + dealloc | |---|---:|---:| | No instrumentation | −43.4% | −36.1% | | `inline(never)` on `add` / `remove` / `update` | +5.5% / +7.5% | +3.0% / −8.1% (inconclusive) | | `inline(never)` on whole `GlobalAlloc` methods | **+19.4% / +21.4%** | **+2.9% / +6.4%** | | **Cold atomic refill/spill helpers (this PR)** | **−4.0% / −3.0%** | **−2.8% / −5.4%** | The full 28-cell turbo-tasks overhead suite was also run twice in reversed order. Large process-level effects changed magnitude or sign between runs, so no conclusion is drawn from that suite. ### Verification - `cargo fmt -- --check` - `cargo clippy -p turbo-tasks-malloc --all-targets -- -D warnings -A deprecated` - `cargo test -p turbo-tasks-malloc` - `cargo check -p turbo-tasks-malloc --all-targets` - `cargo bench -p turbo-tasks-malloc --bench allocation -- --test` - Release `next-swc` builds and Node smoke-loads for every measured arm <!-- NEXT_JS_LLM --> --------- Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com> Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
### What? Collapses three duplicated crate dependencies, so each is built once instead of twice. **−150 KiB** of code Manifests and `Cargo.lock` only — no source changes, and none were needed. **`regress` → 0.11.1** (−133 KiB, the bulk of the win) **`wat` and `wasmparser` → the 239 family** (−13 KiB) **`base64` → one workspace declaration on 0.22** (−1.2 KiB)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )